#! /usr/bin/env python

# TODO: As an optimization measure the database can be sharded based on the username (using hash for example)

# TODO: Modify status of returned entities (it is usually calculated)
# The same for estimated finish time for pomodoros

from wsgiref.simple_server import make_server
from vsws import url_pattern, Controller
from model import *
import re
import time
from barrel import cooper
import libxml2

HOST = '172.16.11.107'
#HOST = 'laptop'
#HOST = 'localhost'
HOMEDIR = "/home/kostya/NetBeansProjects/PomodoroServer/src"
XSLDIR = HOMEDIR + "/xsl"
# TODO: Make it configurable per-user in order to customize appearance
xsl_base = '/ui'

# TODO: Implement document saving (flushing) on a regular basis
doc = libxml2.parseFile("%s/users.xml" % HOMEDIR)
users = Users.fromXml(doc.getRootElement())

re_username = re.compile(r'^[a-zA-Z0-9_\-@.]+$')
re_year = re.compile(r'^\d{4}$')
re_month = re.compile(r'^\d{2}$')
re_day = re.compile(r'^\d{2}$')
re_number = re.compile(r'^\d+$')
re_date = re.compile(r'^\d{4}-\d{2}-\d{2}$')

def get_today ():
	return time.strftime('%Y-%m-%d')

def get_now ():
	return time.strftime('%Y-%m-%dT%H:%M:%S')

# Those are 'invalid' in order to simplify return of 400 in case of syntax error
def invalid_username (user):
	return not re_username.match (user)

def invalid_year (year):
	return not re_year.match (year)

def invalid_month (month):
	return not (re_month.match (month) and int(month) >= 1 and int(month) <= 12)

def invalid_day (day):
	return not (re_day.match (day) and int(day) >= 1 and int(day) <= 31)

def invalid_positive_count (count):
	return invalid_index(count)

def invalid_title (title):
	return not title

def invalid_index (index):
	return not (re_number.match (index) and int(index) >= 1)

def invalid_duration (duration):
	return not (re_number.match (duration) and int(duration) >= 1)

def is_today (year, month, day):
	t = time.localtime()
	return t[0] == int(year) and t[1] == int(month) and t[2] == int(day)

def is_today_plan (plan_node):
	return get_today() == plan_node.prop('date')

def invalid_plan (year, month, day):
	return not is_today (year, month, day)

def invalid_state (state):
	return state not in ['completed', 'failed']

# ***************** Controllers ********************

standard_exception_mapping = {IndexError: 404, AttributeError: 404, NotFoundException: 404, ExistsException: 409, InvalidStateException: 409, InvalidFormatException: 400, ValueError: 400}

@url_pattern("/users", ['GET'], exception_mapping = standard_exception_mapping)
def get_users ():
	return {'content_type': 'text/xml', 'body': users.toXml(doc, True)}

@url_pattern("/users", ['POST'], exception_mapping = standard_exception_mapping)
def create_user (id, name, password, pomodorolen = '25', breaklen = '5'):
	if invalid_username (id) or invalid_title (name) or invalid_title (password) or invalid_duration(pomodorolen) or invalid_duration(breaklen):
		return {'status': 400}
	user = User(id, name, password, get_now(), pomodorolen, breaklen, [])
	Users.createUser(user)
	return {'status': 201, 'content_type': 'text/xml', 'body': user.toXml(doc, True)}

@url_pattern('/users/${username}', ['GET'], exception_mapping = standard_exception_mapping)
def get_user (username):
	if invalid_username (username):
		return {'status': 400}
	user = users.getUser(username)
	return {'content_type': 'text/xml', 'body': user.toXml(doc, True)}

@url_pattern('/users/${username}', ['PUT'], exception_mapping = standard_exception_mapping)
def modify_user (username, name = None, password = None, pomodorolen = None, breaklen = None):
	if invalid_username (username) or (name and invalid_title (name)) or (password and invalid_title (password)) or (pomodorolen and invalid_duration(pomodorolen)) or (breaklen and invalid_duration(breaklen)):
		return {'status': 400}
	user = users.getUser(username)
	if name:
		user.name = name
	if password:
		user.password = password
	if pomodorolen:
		user.pomodoroLen = pomodoroLen
	if breaklen:
		user.breakLen = breakLen
	return {'status': 200, 'content_type': 'text/xml', 'body': user.toXml(doc, True)}

# TODO: Check for 404 the other way
@url_pattern('/users/${username}', ['DELETE'], exception_mapping = standard_exception_mapping)
def delete_user (username):
	if invalid_username (username):
		return {'status': 400}
	user = users.deleteUser(username)
	return {'status': 200, 'content_type': 'text/xml', 'body': user.toXml(doc, True)}

@url_pattern("/users/${username}/plans", ['GET'], exception_mapping = standard_exception_mapping)
@url_pattern("/users/${username}/plans/${year}", ['GET'], exception_mapping = standard_exception_mapping)
@url_pattern("/users/${username}/plans/${year}/${month}", ['GET'], exception_mapping = standard_exception_mapping)
def get_plans (username, year = None, month = None):
	if invalid_username (username) or (year and invalid_year (year)) or (month and invalid_month (month)):
		return {'status': 400}
	if not year:
		plans = users.getUser(username).getAllPlans()
	else:
		plans = users.getUser(username).getPlansForYear(year)
	if month:
		plans = users.getUser(username).getPlansForMonth(year, month)
	return {'content_type': 'text/xml', 'body': plans.toXml(doc, True)}

# TODO: Handle 404
@url_pattern("/users/${username}/plans", ['POST'], exception_mapping = standard_exception_mapping)
def create_plan (username):
	if invalid_username (username):
		return {'status': 400}
	plan = Plan(get_today(), "new", [])
	users.getUser(username).createPlan(plan)
	return {'status': 201, 'content_type': 'text/xml', 'body': plan.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}", ['GET'], exception_mapping = standard_exception_mapping)
def get_plan (username, year, month, day):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day):
		return {'status': 400}
	plan = users.getUser(username).getPlan(year, month, day)
	return {'content_type': 'text/xml', 'body': plan.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems", ['GET'], exception_mapping = standard_exception_mapping)
def get_workitems (username, year, month, day):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day):
		return {'status': 400}
	workItems = users.getUser(username).getPlan(year, month, day).getWorkItems()
	return {'content_type': 'text/xml', 'body': workItems.toXml(doc, True)}

# TODO: Move all checks to model and only catch exceptions here
@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems", ['POST'], exception_mapping = standard_exception_mapping)
def create_workitem (username, year, month, day, title, pomodoros):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_title(title) or invalid_positive_count(pomodoros):
		return {'status': 400}
	if invalid_plan(year, month, day): return {'status': 409}
	plan = users.getUser(username).getPlan(year, month, day)
	if plan.getStatus() in ['new', 'started']:
		section = ''
		if plan_state == 'started':
			section = 'unplanned'
		else:
			section = 'planned'
		item = WorkItem(section, 'new', title, [])
		for i in range(0, int(pomodoros)):
			item.createPomodoro(Pomodoro("true", "new", None, None, [], []))
		plan.createWorkItem(item)
		return {'status': 201, 'content_type': 'text/xml', 'body': item.toXml(doc, True)}
	else:
		return {'status': 409}
	
@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}", ['GET'], exception_mapping = standard_exception_mapping)
def get_workitem (username, year, month, day, workitem):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem):
		return {'status': 400}
	item = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1]
	return {'content_type': 'text/xml', 'body': item.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}", ['PUT'], exception_mapping = standard_exception_mapping)
def modify_workitem (username, year, month, day, workitem, state):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_state (state):
		return {'status': 400}
	
	item = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1]
	# TODO: Check rules
	oldstate = item.getStatus()
	if oldstate == 'new':
		if not state:
			state = 'started'
		if state == 'started':
			# TODO: Start first pomodoro
			# Check if no other pomodoros are started in the plan
			pass
		elif state in ['failed', 'completed']:
			pass
		else:
			return {'status': 409}
	elif oldstate == 'started':
		if not state:
			state = 'completed'
		if state == 'failed':
			# TODO: Abort a started pomodoro, if exists
			pass
		elif state == 'completed':
			# TODO: Check if a started pomodoro exist
			# Throw 409 if yes
			pass
		else:
			return {'status': 409}
	else:
		return {'status': 409}

	item.status = status
	return {'status': 200, 'content_type': 'text/xml', 'body': item.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros", ['GET'], exception_mapping = standard_exception_mapping)
def get_pomodoros (username, year, month, day, workitem):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem):
		return {'status': 400}
	result = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()
	return {'content_type': 'text/xml', 'body': result.toXml(doc, True)}

def get_started_pomodoro (plan):
	for workitem in plan.workItems:
		for pomodoro in workitem.pomodoros:
			if pomodoro.status == 'started':
				return pomodoro

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros", ['POST'], exception_mapping = standard_exception_mapping)
def create_pomodoro (username, year, month, day, workitem):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem):
		return {'status': 400}
	if invalid_plan(year, month, day): return {'status': 409}
	item = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1]
	if item.getStatus() in ['new', 'started']:
		planned = (item.parent.getStatus() == 'started')
		pomodoro = Pomodoro(str(planned).lower(), 'new', None, None, [], [])
		item.createPomodoro(pomodoro)
		return {'status': 201, 'content_type': 'text/xml', 'body': pomodoro.toXml(doc, True)}
	else:
		return {'status': 409}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}", ['GET'], exception_mapping = standard_exception_mapping)
def get_pomodoro (username, year, month, day, workitem, pomodoro):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro):
		return {'status': 400}
	pomodoro = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1]
	return {'content_type': 'text/xml', 'body': pomodoro.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}", ['PUT'], exception_mapping = standard_exception_mapping)
def modify_pomodoro (username, year, month, day, workitem, pomodoro, state):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro) or invalid_state (state):
		return {'status': 400}
	# Increments pomodoro status. When pomodoro is registered, its status is set to new. 
	# When started, pomodoro's status is set to started. 
	# Any further status changes will seal the pomodoro. 
	# There can not exist more than one pomodoro in started state for the whole plan.

	# 1. Check that pomodoro exists
	# 2. Check it is new or started (here also plan state is checked)
	# 3. If new, then check there are no other pomodoros in started state
	# 4. Increment status and change timestamp
	
	pomodoro = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1]
	oldstate = pomodoro.getStatus()
	if oldstate == 'new':
		if not state:
			state = 'started'
		if state == 'started' and not get_started_pomodoro (pomodoro.parent.parent):
			pomodoro.setStart(get_now())
			pomodoro.setStatus(state)
			return {'status': 200, 'content_type': 'text/xml', 'body': pomodoro.toXml(doc, True)}
		else:
			return {'status': 409}
	elif oldstate == 'started':
		if not state:
			state = 'completed'
		if state in ['completed', 'failed']:
			pomodoro.setFinish(get_now())
			pomodoro.setStatus(state)
			return {'status': 200, 'content_type': 'text/xml', 'body': pomodoro.toXml(doc, True)}
		else:
			return {'status': 409}
	else:
		return {'status': 409}
	

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/interruptions", ['GET'], exception_mapping = standard_exception_mapping)
def get_interruptions (username, year, month, day, workitem, pomodoro):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro):
		return {'status': 400}
	result = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1].getInterruptions()
	return {'content_type': 'text/xml', 'body': result.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/interruptions", ['POST'], ['comment', 'duration'], standard_exception_mapping)
def post_interruption (username, year, month, day, workitem, pomodoro, comment, duration):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro) or (duration and invalid_duration (duration)):
		return {'status': 400}
	if invalid_plan(year, month, day): return {'status': 409}
	pomodoro = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1]
	if pomodoro.getStatus() == 'started':
		interruption = Interruption(get_now(), duration, comment)
		pomodoro.createInterruption(interruption)
		return {'status': 201, 'content_type': 'text/xml', 'body': interruption.toXml(doc, True)}
	else:
		return {'status': 409}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/interruptions/${interruption}", ['GET'], exception_mapping = standard_exception_mapping)
def get_interruption (username, year, month, day, workitem, pomodoro, interruption):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro) or invalid_index (interruption):
		return {'status': 400}
	result = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1].getInterruptions()[int(interruption) - 1]
	return {'content_type': 'text/xml', 'body': result.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/messages", ['GET'], exception_mapping = standard_exception_mapping)
def get_messages (username, year, month, day, workitem, pomodoro):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro):
		return {'status': 400}
	result = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1].getMessages()
	return {'content_type': 'text/xml', 'body': result.toXml(doc, True)}

@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/messages", ['POST'], exception_mapping = standard_exception_mapping)
def post_message (username, year, month, day, workitem, pomodoro, text):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro):
		return {'status': 400}
	if invalid_plan(year, month, day): return {'status': 409}
	pomodoro = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1]
	if pomodoro.getStatus() == 'started':
		message = Message('kostya', text, get_now())
		pomodoro.createMessage(message)
		return {'status': 201, 'content_type': 'text/xml', 'body': message.toXml(doc, True)}
	else:
		return {'status': 409}
	
@url_pattern("/users/${username}/plans/${year}/${month}/${day}/workitems/${workitem}/pomodoros/${pomodoro}/messages/${message}", ['GET'], exception_mapping = standard_exception_mapping)
def get_message (username, year, month, day, workitem, pomodoro, message):
	if invalid_username (username) or invalid_year (year) or invalid_month (month) or invalid_day (day) or invalid_index (workitem) or invalid_index (pomodoro) or invalid_index (message):
		return {'status': 400}
	result = users.getUser(username).getPlan(year, month, day).getWorkItems()[int(workitem) - 1].getPomodoros()[int(pomodoro) - 1].getMessages()[int(message) - 1]
	return {'content_type': 'text/xml', 'body': result.toXml(doc, True)}


# ***************** Built-in UI handling ********************

@url_pattern("/ui", ['GET'])
def get_ui_index ():
	result = open("%s/ui/%s" % (HOMEDIR, 'ui.html'), 'r').read()
	return {'content_type': 'text/html', 'body': result}

@url_pattern("/ui/${filename}.xsl", ['GET'])
def get_ui_file (filename):
	result = open("%s/ui/%s.xsl" % (HOMEDIR, filename), 'r').read()
	return {'content_type': 'text/xsl', 'body': result}

@url_pattern("/ui/${filename}.css", ['GET'])
def get_ui_file (filename):
	result = open("%s/ui/%s.css" % (HOMEDIR, filename), 'r').read()
	return {'content_type': 'text/css', 'body': result}

@url_pattern("/ui/${filename}.js", ['GET'])
def get_ui_file (filename):
	result = open("%s/ui/%s.js" % (HOMEDIR, filename), 'r').read()
	return {'content_type': 'text/javascript', 'body': result}


# ***************** Server initialization ********************

# Get all logins...
# TODO: Updated logins won't work until server restart
logins = []
for user in users.getUsers():
    logins += [(user.getId(), user.getPassword())]

@cooper.basicauth(users=logins, realm='Pomodoro_Server_Login')
def authController(env, start_response):
	c = Controller()
	# env.get('com.saddi.service.session', {})
	return c(env, start_response)

while True:
	make_server(HOST, 8051, authController).handle_request()
